/*******************************************************************************
 * This file is part of Crunch Network.
 *
 * Crunch Network is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Network is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Network.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.network.util;

/**
 * Utility class used to compress network data.
 */
public class Compressor {
	/**
	 * Compresses a quantized unsigned normalized float value of size {@code bitCount}.
	 *
	 * @param   value the unsigned normalized float value to compress. The value is automatically clamped to the range
	 *          {@code [0.0f, 1.0f]}.
	 * @param   bitCount the number of bits to compress into; must be between 1 and 32.
	 * @return  an integer with the first {@code bitCount} bits containing the compressed value.
	 */
	public static int compressUNormFloat(float value, int bitCount) {
		if (bitCount < 1 || bitCount > 32) {
			throw new IllegalArgumentException("Invalid bit count");
		}
		// [0,1] is quantized into 2^bitCount equally sized half-open sub-intervals
		// since the upper range of [0,1] is closed, the last sub-interval is closed as well
		long maxValue = (1 << bitCount) - 1;
		long bits = (long) Math.floor((double) value * (double) (maxValue + 1));
		// if value is exactly maxValue, bits will be maxValue+1, so we clamp to avoid this
		bits = Math.max(0, Math.min(maxValue, bits));
		return (int) bits;
	}

	/**
	 * Decompresses a quantized unsigned normalized float value of size {@code bitCount}.
	 *
	 * @param   bits an integer with the first {@code bitCount} bits containing the compressed value.
	 * @param   bitCount the number of bits to read; must be between 1 and 32.
	 * @return  an unsigned normalized float value decompressed from {@code bits}. The returned value will be in the
	 *          range {@code [0.0f, 1.0f]}.
	 */
	public static float decompressUNormFloat(int bits, int bitCount) {
		if (bitCount < 1 || bitCount > 32) {
			throw new IllegalArgumentException("Invalid bit count");
		}
		long maxValue = (1 << bitCount) - 1;
		// 0 is mapped to 0.0f
		// maxValue is mapped to 1.0f
		return (float) ((double) (bits & 0x00000000ffffffffL) / (double) maxValue);
	}

	/**
	 * Compresses a quantized signed normalized float value of size {@code bitCount}.
	 *
	 * @param   value the signed normalized float value to compress. The value is automatically clamped to the range
	 *          {@code [-1.0f, 1.0f]}.
	 * @param   bitCount the number of bits to compress into; must be between 1 and 32.
	 * @return  an integer with the first {@code bitCount} bits containing the compressed value.
	 */
	public static int compressSNormFloat(float value, int bitCount) {
		return compressUNormFloat(value * 0.5f + 0.5f, bitCount);
	}

	/**
	 * Decompresses a quantized signed normalized float value of size {@code bitCount}.
	 *
	 * @param   bits an integer with the first {@code bitCount} bits containing the compressed value.
	 * @param   bitCount the number of bits to read; must be between 1 and 32.
	 * @return  a signed normalized float value decompressed from {@code bits}. The returned value will be in the
	 *          range {@code [-1.0f, 1.0f]}.
	 */
	public static float decompressSNormFloat(int bits, int bitCount) {
		return decompressUNormFloat(bits, bitCount) * 2.0f - 1.0f;
	}

	/**
	 * Compresses a quantized wrapped normalized float value of size {@code bitCount}. This method differs from {@link
	 * Compressor#compressUNormFloat(float, int)} in that the domain of this method is a half-open interval.
	 *
	 * @param   value the wrapped normalized float value to compress. The value is automatically wrapped to the range
	 *          {@code [0.0f, 1.0f]}.
	 * @param   bitCount the number of bits to compress into; must be between 1 and 32.
	 * @return  an integer with the first {@code bitCount} bits containing the compressed value.
	 */
	public static int compressWrappedNormFloat(float value, int bitCount) {
		if (bitCount < 1 || bitCount > 32) {
			throw new IllegalArgumentException("Invalid bit count");
		}
		// wrap the value so it's in the range [0,1)
		value -= (float) Math.floor(value);
		long maxValue = (1 << bitCount) - 1;
		long bits = Math.round((double) value * (double) maxValue);
		// if the value is close to 1, it could have rounded up to maxValue
		// fix this by performing modulo (using AND for performance)
		bits &= ~(0xffffffffffffffffL << bitCount);
		return (int) bits;
	}

	/**
	 * Decompresses a quantized wrapped normalized float value of size {@code bitCount}. This method differs from {@link
	 * Compressor#decompressUNormFloat(int, int)} in that the range of this method is a half-open interval.
	 *
	 * @param   bits an integer with the first {@code bitCount} bits containing the compressed value.
	 * @param   bitCount the number of bits to read; must be between 1 and 32.
	 * @return  a wrapped normalized float value decompressed from {@code bits}. The returned value will be in the
	 *          range {@code [0.0f, 1.0f]}.
	 */
	public static float decompressWrappedNormFloat(int bits, int bitCount) {
		if (bitCount < 1 || bitCount > 32) {
			throw new IllegalArgumentException("Invalid bit count");
		}
		long maxValue = 1 << bitCount;
		// 0 is mapped to 0.0f
		// maxValue is mapped to 1.0f
		return (float) ((double) (bits & 0x00000000ffffffffL) / (double) maxValue);
	}
}
